/**
 * OAuth Token Manager for Claude AI
 * Handles OAuth tokens from .authtoken file
 */

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import axios from 'axios';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

class OAuthManager {
  constructor() {
    this.tokens = []; // Array to hold multiple tokens
    this.currentTokenIndex = 0; // Index of currently active token
    this.tokenFilePath = path.join(path.dirname(__dirname), '.authtoken');
    this.refreshEndpoint = 'https://console.anthropic.com/v1/oauth/token'; // Claude OAuth refresh endpoint
    this.clientId = '9d1c250a-e61b-44d9-88ed-5944d1962f5e'; // Claude OAuth client ID
    this.refreshTimers = new Map(); // Map to hold refresh timers for each token
    this.isRefreshing = new Set(); // Set to track which tokens are being refreshed
    this.loadTokens();
    this.setupAutoRefresh();
  }

  /**
   * Load OAuth tokens from .authtoken file
   */
  loadTokens() {
    try {
      if (fs.existsSync(this.tokenFilePath)) {
        const fileContent = fs.readFileSync(this.tokenFilePath, 'utf8');
        const data = JSON.parse(fileContent);
        
        // Handle both old single token format and new array format
        if (Array.isArray(data)) {
          this.tokens = data.map(tokenEntry => ({
            ...tokenEntry.claudeAiOauth,
            name: tokenEntry.name || 'Unknown' // Preserve the name from parent object
          })).filter(Boolean);
          console.log(`✅ ${this.tokens.length} OAuth tokens loaded from .authtoken file`);
          this.tokens.forEach((token, index) => {
            console.log(`  Token ${index + 1}:`);
            console.log(`    Name: ${token.name || 'Unknown'}`);
            console.log(`    Subscription: ${token.subscriptionType}`);
            console.log(`    Expires at: ${new Date(token.expiresAt).toISOString()}`);
            console.log(`    Scopes: ${token.scopes.join(', ')}`);
          });
        } else if (data.claudeAiOauth) {
          // Legacy single token format
          this.tokens = [data.claudeAiOauth];
          console.log('✅ Single OAuth token loaded from .authtoken file (legacy format)');
          console.log(`  Subscription: ${this.tokens[0].subscriptionType}`);
          console.log(`  Expires at: ${new Date(this.tokens[0].expiresAt).toISOString()}`);
          console.log(`  Scopes: ${this.tokens[0].scopes.join(', ')}`);
        } else {
          this.tokens = [];
        }
        
        // Start with first valid token
        this.currentTokenIndex = 0;
      } else {
        console.log('⚠️  No .authtoken file found');
        this.tokens = [];
      }
    } catch (error) {
      console.error('❌ Error loading OAuth tokens:', error.message);
      this.tokens = [];
    }
  }

  /**
   * Get the current access token
   */
  async getAccessToken() {
    if (!this.tokens.length) {
      return null;
    }

    const currentToken = this.getCurrentToken();
    if (!currentToken) {
      return null;
    }

    // Check if token is expired or about to expire (within 5 minutes)
    const expiresIn = currentToken.expiresAt - Date.now();
    if (expiresIn <= 5 * 60 * 1000) { // 5 minutes buffer
      console.log(`⚠️  OAuth token ${this.currentTokenIndex + 1} is expired or expiring soon, refreshing...`);
      await this.refreshToken(this.currentTokenIndex);
    }

    return currentToken.accessToken;
  }

  /**
   * Get current active token
   */
  getCurrentToken() {
    if (!this.tokens.length || this.currentTokenIndex >= this.tokens.length) {
      return null;
    }
    return this.tokens[this.currentTokenIndex];
  }

  /**
   * Switch to next available token when current one hits rate limits
   */
  async switchToNextToken(reason = 'rate limit exceeded') {
    if (this.tokens.length <= 1) {
      console.log('❌ No alternative tokens available for switching');
      return false;
    }

    const oldIndex = this.currentTokenIndex;
    console.log(`🔄 Switching from token ${oldIndex + 1} (${reason})`);

    // Try all other tokens
    for (let i = 1; i < this.tokens.length; i++) {
      const nextIndex = (this.currentTokenIndex + i) % this.tokens.length;
      const nextToken = this.tokens[nextIndex];

      // Check if this token is expired
      const expiresIn = nextToken.expiresAt - Date.now();
      if (expiresIn <= 0) {
        console.log(`⏭️  Token ${nextIndex + 1} is expired, trying next...`);
        continue;
      }

      // Check if this token needs refresh but is still valid
      if (expiresIn <= 5 * 60 * 1000) { // Within 5 minutes
        console.log(`🔄 Token ${nextIndex + 1} needs refresh, refreshing before use...`);
        const refreshed = await this.refreshToken(nextIndex);
        if (!refreshed) {
          console.log(`❌ Failed to refresh token ${nextIndex + 1}, trying next...`);
          continue;
        }
      }

      // Switch to this token
      this.currentTokenIndex = nextIndex;
      console.log(`✅ Switched to token ${nextIndex + 1}`);
      return true;
    }

    console.log('❌ No valid alternative tokens found');
    return false;
  }

  /**
   * Get the refresh token for current token
   */
  getRefreshToken(tokenIndex = this.currentTokenIndex) {
    const token = this.tokens[tokenIndex];
    return token?.refreshToken || null;
  }

  /**
   * Check if the current token is expired
   */
  isTokenExpired(tokenIndex = this.currentTokenIndex) {
    const token = this.tokens[tokenIndex];
    if (!token || !token.expiresAt) {
      return true;
    }
    return Date.now() >= token.expiresAt;
  }

  /**
   * Get authorization header for OAuth
   */
  async getAuthorizationHeader() {
    const token = await this.getAccessToken();
    if (token) {
      // OAuth tokens use Bearer authentication
      return `Bearer ${token}`;
    }
    return null;
  }

  /**
   * Check if OAuth is available
   */
  isAvailable() {
    return this.tokens.length > 0;
  }

  /**
   * Get subscription type for current token
   */
  getSubscriptionType(tokenIndex = this.currentTokenIndex) {
    const token = this.tokens[tokenIndex];
    return token?.subscriptionType || 'unknown';
  }

  /**
   * Get summary of all tokens
   */
  getAllTokensStatus() {
    return this.tokens.map((token, index) => ({
      index: index,
      active: index === this.currentTokenIndex,
      name: token.name || 'Unknown',
      subscriptionType: token.subscriptionType,
      scopes: token.scopes || [],
      accessToken: token.accessToken,
      expiresAt: new Date(token.expiresAt).toISOString(),
      expiresInMinutes: Math.round((token.expiresAt - Date.now()) / 1000 / 60),
      isExpired: Date.now() >= token.expiresAt,
      needsRefresh: (token.expiresAt - Date.now()) <= 5 * 60 * 1000,
      lastRefreshed: token.lastRefreshed || null,
      refreshCount: token.refreshCount || 0,
      error: token.error || null
    }));
  }

  /**
   * Reload tokens from file
   */
  reload() {
    this.cleanup(); // Clean up existing timers
    this.loadTokens();
    this.setupAutoRefresh();
  }

  /**
   * Save updated tokens back to file
   */
  saveTokens(tokenIndex, newTokenData) {
    try {
      // Read current file to preserve the array structure and names
      const fileContent = fs.readFileSync(this.tokenFilePath, 'utf8');
      const data = JSON.parse(fileContent);

      if (Array.isArray(data) && tokenIndex < data.length) {
        // Preserve the name from parent entry
        const name = data[tokenIndex].name || newTokenData.name;
        data[tokenIndex].claudeAiOauth = newTokenData;
        this.tokens[tokenIndex] = {
          ...newTokenData,
          name: name
        };
      } else {
        throw new Error('Invalid token index or file structure');
      }

      fs.writeFileSync(this.tokenFilePath, JSON.stringify(data, null, 4));
      console.log(`✅ Token ${tokenIndex + 1} updated and saved`);
      console.log(`  New expiry: ${new Date(newTokenData.expiresAt).toISOString()}`);
      
      // Reset auto-refresh timer for this token
      this.setupAutoRefreshForToken(tokenIndex);
    } catch (error) {
      console.error(`❌ Error saving token ${tokenIndex + 1}:`, error.message);
    }
  }

  /**
   * Refresh the OAuth token using refresh token
   */
  async refreshToken(tokenIndex = this.currentTokenIndex) {
    const token = this.tokens[tokenIndex];
    if (!token || !token.refreshToken) {
      console.error(`❌ Cannot refresh token ${tokenIndex + 1}: no refresh token available`);
      return false;
    }

    // Prevent multiple simultaneous refresh attempts for same token
    if (this.isRefreshing.has(tokenIndex)) {
      console.log(`⏳ Token ${tokenIndex + 1} refresh already in progress...`);
      // Wait for the current refresh to complete
      await new Promise(resolve => {
        const checkInterval = setInterval(() => {
          if (!this.isRefreshing.has(tokenIndex)) {
            clearInterval(checkInterval);
            resolve();
          }
        }, 100);
      });
      return true;
    }

    this.isRefreshing.add(tokenIndex);
    
    try {
      console.log(`🔄 Refreshing OAuth token ${tokenIndex + 1}...`);
      
      const response = await axios.post(this.refreshEndpoint, {
        grant_type: 'refresh_token',
        refresh_token: token.refreshToken,
        client_id: this.clientId
      }, {
        headers: {
          'Content-Type': 'application/json',
          'User-Agent': 'anthropic-proxy/1.0'
        },
        timeout: 30000
      });

      if (response.data && response.data.access_token) {
        // Update token data with new tokens
        const newTokenData = {
          ...token,
          accessToken: response.data.access_token,
          refreshToken: response.data.refresh_token || token.refreshToken,
          expiresAt: Date.now() + (response.data.expires_in * 1000),
          scopes: response.data.scopes || token.scopes,
          subscriptionType: response.data.subscription_type || token.subscriptionType,
          lastRefreshed: new Date().toISOString(),
          refreshCount: (token.refreshCount || 0) + 1,
          error: null // Clear any previous errors
        };

        // Save to file
        this.saveTokens(tokenIndex, newTokenData);
        console.log(`✅ Token ${tokenIndex + 1} refreshed successfully`);
        return true;
      } else {
        console.error(`❌ Invalid refresh response for token ${tokenIndex + 1}:`, response.data);
        return false;
      }
    } catch (error) {
      console.error(`❌ Error refreshing token ${tokenIndex + 1}:`, error.message);
      
      // Store error in token data
      if (this.tokens[tokenIndex]) {
        this.tokens[tokenIndex].error = error.message;
      }
      
      // If refresh fails with 401, the refresh token might be invalid
      if (error.response && error.response.status === 401) {
        console.error(`⚠️  Refresh token ${tokenIndex + 1} may be invalid. Manual re-authentication may be required.`);
        if (this.tokens[tokenIndex]) {
          this.tokens[tokenIndex].error = 'Invalid refresh token - re-authentication required';
        }
      }
      
      return false;
    } finally {
      this.isRefreshing.delete(tokenIndex);
    }
  }

  /**
   * Setup automatic token refresh for all tokens
   */
  setupAutoRefresh() {
    // Clear existing timers
    for (const [tokenIndex, timer] of this.refreshTimers) {
      clearTimeout(timer);
    }
    this.refreshTimers.clear();

    // Setup refresh timer for each token
    this.tokens.forEach((token, index) => {
      this.setupAutoRefreshForToken(index);
    });
  }

  /**
   * Setup automatic token refresh for a specific token
   */
  setupAutoRefreshForToken(tokenIndex) {
    // Clear existing timer for this token
    if (this.refreshTimers.has(tokenIndex)) {
      clearTimeout(this.refreshTimers.get(tokenIndex));
      this.refreshTimers.delete(tokenIndex);
    }

    const token = this.tokens[tokenIndex];
    if (!token || !token.expiresAt) {
      return;
    }

    // Calculate when to refresh (10 minutes before expiry)
    const expiresIn = token.expiresAt - Date.now();
    const refreshIn = Math.max(0, expiresIn - 10 * 60 * 1000); // 10 minutes before expiry

    if (refreshIn > 0) {
      console.log(`⏰ Auto-refresh for token ${tokenIndex + 1} scheduled in ${Math.round(refreshIn / 1000 / 60)} minutes`);
      
      const timer = setTimeout(async () => {
        console.log(`⏰ Auto-refresh timer triggered for token ${tokenIndex + 1}`);
        await this.refreshToken(tokenIndex);
      }, refreshIn);
      
      this.refreshTimers.set(tokenIndex, timer);
    } else if (expiresIn > 0) {
      // Token expires soon, refresh immediately
      console.log(`⚠️  Token ${tokenIndex + 1} expires soon, refreshing immediately...`);
      this.refreshToken(tokenIndex);
    }
  }

  /**
   * Manually trigger token refresh
   */
  async forceRefresh(tokenIndex = this.currentTokenIndex) {
    console.log(`🔄 Manually triggering token ${tokenIndex + 1} refresh...`);
    return await this.refreshToken(tokenIndex);
  }

  /**
   * Manually trigger refresh for all tokens
   */
  async forceRefreshAll() {
    console.log('🔄 Manually triggering refresh for all tokens...');
    const results = [];
    for (let i = 0; i < this.tokens.length; i++) {
      const result = await this.refreshToken(i);
      results.push(result);
    }
    return results.every(r => r); // Return true only if all refreshes succeeded
  }

  /**
   * Get token expiry information for current token
   */
  getExpiryInfo(tokenIndex = this.currentTokenIndex) {
    const token = this.tokens[tokenIndex];
    if (!token || !token.expiresAt) {
      return null;
    }

    const now = Date.now();
    const expiresAt = token.expiresAt;
    const expiresIn = expiresAt - now;

    return {
      tokenIndex: tokenIndex,
      expiresAt: new Date(expiresAt).toISOString(),
      expiresInMinutes: Math.round(expiresIn / 1000 / 60),
      isExpired: expiresIn <= 0,
      needsRefresh: expiresIn <= 5 * 60 * 1000 // Within 5 minutes
    };
  }

  /**
   * Get expiry info for all tokens
   */
  getAllExpiryInfo() {
    return this.tokens.map((_, index) => this.getExpiryInfo(index)).filter(Boolean);
  }

  /**
   * Add a new token from JSON string
   */
  addToken(tokenData, options = {}) {
    try {
      let parsedData;
      
      // Parse if it's a string
      if (typeof tokenData === 'string') {
        parsedData = JSON.parse(tokenData);
      } else {
        parsedData = tokenData;
      }

      // Extract OAuth data
      let oauthData;
      if (parsedData.claudeAiOauth) {
        oauthData = parsedData.claudeAiOauth;
      } else if (parsedData.accessToken) {
        oauthData = parsedData;
      } else {
        throw new Error('Invalid token format. Must contain claudeAiOauth or accessToken');
      }

      // Ensure required fields
      if (!oauthData.accessToken || !oauthData.refreshToken) {
        throw new Error('Token must contain accessToken and refreshToken');
      }

      // Check if file exists, create it if it doesn't
      let data = [];
      if (fs.existsSync(this.tokenFilePath)) {
        const fileContent = fs.readFileSync(this.tokenFilePath, 'utf8');
        data = JSON.parse(fileContent);
      } else {
        console.log('📝 Creating new .authtoken file...');
        // Ensure it's an array format for new files
        data = [];
      }

      // Check for duplicates
      const duplicateCheck = {
        nameIndex: -1,
        tokenIndex: -1,
        hasDuplicateName: false,
        hasDuplicateToken: false
      };

      const newName = parsedData.name || `User ${data.length + 1}`;
      
      data.forEach((entry, index) => {
        if (entry.name === newName) {
          duplicateCheck.hasDuplicateName = true;
          duplicateCheck.nameIndex = index;
        }
        if (entry.claudeAiOauth && 
            entry.claudeAiOauth.accessToken === oauthData.accessToken) {
          duplicateCheck.hasDuplicateToken = true;
          duplicateCheck.tokenIndex = index;
        }
      });

      // If duplicates found and not force overwrite, return duplicate info
      if ((duplicateCheck.hasDuplicateName || duplicateCheck.hasDuplicateToken) && !options.force) {
        return {
          success: false,
          duplicate: true,
          duplicateName: duplicateCheck.hasDuplicateName,
          duplicateToken: duplicateCheck.hasDuplicateToken,
          nameIndex: duplicateCheck.nameIndex,
          tokenIndex: duplicateCheck.tokenIndex,
          existingName: duplicateCheck.hasDuplicateName ? data[duplicateCheck.nameIndex].name : null,
          message: duplicateCheck.hasDuplicateName && duplicateCheck.hasDuplicateToken 
            ? 'Both name and token already exist' 
            : duplicateCheck.hasDuplicateName 
              ? 'Name already exists' 
              : 'Token already exists'
        };
      }

      // Create new token entry
      const newEntry = {
        name: newName,
        claudeAiOauth: {
          accessToken: oauthData.accessToken,
          refreshToken: oauthData.refreshToken,
          expiresAt: oauthData.expiresAt || Date.now() + 3600000, // Default 1 hour if not provided
          scopes: oauthData.scopes || ['user:inference', 'user:profile'],
          subscriptionType: oauthData.subscriptionType || 'unknown',
          lastRefreshed: new Date().toISOString(),
          refreshCount: 0,
          error: null
        }
      };

      let finalIndex;
      
      // Handle overwrite if force is true
      if (options.force && (duplicateCheck.hasDuplicateToken || duplicateCheck.hasDuplicateName)) {
        // If duplicate token exists, overwrite at that index
        if (duplicateCheck.hasDuplicateToken) {
          data[duplicateCheck.tokenIndex] = newEntry;
          finalIndex = duplicateCheck.tokenIndex;
        }
        // If only duplicate name exists, overwrite at that index
        else if (duplicateCheck.hasDuplicateName) {
          data[duplicateCheck.nameIndex] = newEntry;
          finalIndex = duplicateCheck.nameIndex;
        }
      } else {
        // Add to array
        data.push(newEntry);
        finalIndex = data.length - 1;
      }

      // Save to file
      fs.writeFileSync(this.tokenFilePath, JSON.stringify(data, null, 4));

      // Reload tokens
      this.reload();

      return {
        success: true,
        index: finalIndex,
        name: newEntry.name,
        overwritten: options.force && (duplicateCheck.hasDuplicateToken || duplicateCheck.hasDuplicateName)
      };
    } catch (error) {
      console.error('❌ Error adding token:', error.message);
      return {
        success: false,
        error: error.message
      };
    }
  }

  /**
   * Add multiple tokens from JSON array
   */
  addTokensBulk(tokensData) {
    try {
      let parsedData;
      
      // Parse if it's a string
      if (typeof tokensData === 'string') {
        parsedData = JSON.parse(tokensData);
      } else {
        parsedData = tokensData;
      }

      // Ensure it's an array
      if (!Array.isArray(parsedData)) {
        throw new Error('Bulk data must be an array');
      }

      // Check if file exists, create it if it doesn't
      let data = [];
      if (fs.existsSync(this.tokenFilePath)) {
        const fileContent = fs.readFileSync(this.tokenFilePath, 'utf8');
        data = JSON.parse(fileContent);
      } else {
        console.log('📝 Creating new .authtoken file...');
        // Ensure it's an array format for new files
        data = [];
      }

      const addedTokens = [];

      // Process each token
      parsedData.forEach((tokenData, index) => {
        let oauthData;
        if (tokenData.claudeAiOauth) {
          oauthData = tokenData.claudeAiOauth;
        } else if (tokenData.accessToken) {
          oauthData = tokenData;
        } else {
          throw new Error(`Token at index ${index} has invalid format`);
        }

        // Ensure required fields
        if (!oauthData.accessToken || !oauthData.refreshToken) {
          throw new Error(`Token at index ${index} must contain accessToken and refreshToken`);
        }

        // Create new token entry
        const newEntry = {
          name: tokenData.name || `User ${data.length + 1}`,
          claudeAiOauth: {
            accessToken: oauthData.accessToken,
            refreshToken: oauthData.refreshToken,
            expiresAt: oauthData.expiresAt || Date.now() + 3600000,
            scopes: oauthData.scopes || ['user:inference', 'user:profile'],
            subscriptionType: oauthData.subscriptionType || 'unknown',
            lastRefreshed: new Date().toISOString(),
            refreshCount: 0,
            error: null
          }
        };

        data.push(newEntry);
        addedTokens.push({
          index: data.length - 1,
          name: newEntry.name
        });
      });

      // Save to file
      fs.writeFileSync(this.tokenFilePath, JSON.stringify(data, null, 4));

      // Reload tokens
      this.reload();

      return {
        success: true,
        added: addedTokens.length,
        tokens: addedTokens
      };
    } catch (error) {
      console.error('❌ Error adding tokens in bulk:', error.message);
      throw error;
    }
  }

  /**
   * Delete a token by index
   */
  deleteToken(tokenIndex) {
    try {
      // Check if file exists
      if (!fs.existsSync(this.tokenFilePath)) {
        throw new Error('.authtoken file does not exist');
      }

      // Read current file
      const fileContent = fs.readFileSync(this.tokenFilePath, 'utf8');
      const data = JSON.parse(fileContent);

      if (!Array.isArray(data)) {
        throw new Error('Token file is not in array format');
      }

      if (tokenIndex < 0 || tokenIndex >= data.length) {
        throw new Error(`Invalid token index: ${tokenIndex}`);
      }

      // Get the name before deletion
      const deletedName = data[tokenIndex].name;

      // Remove the token
      data.splice(tokenIndex, 1);

      // Save to file
      fs.writeFileSync(this.tokenFilePath, JSON.stringify(data, null, 4));

      // If we deleted the current token, switch to the first one
      if (this.currentTokenIndex === tokenIndex) {
        this.currentTokenIndex = 0;
      } else if (this.currentTokenIndex > tokenIndex) {
        // Adjust index if we deleted a token before the current one
        this.currentTokenIndex--;
      }

      // Reload tokens
      this.reload();

      return {
        success: true,
        deletedIndex: tokenIndex,
        deletedName: deletedName
      };
    } catch (error) {
      console.error('❌ Error deleting token:', error.message);
      throw error;
    }
  }

  /**
   * Cleanup on shutdown
   */
  cleanup() {
    // Clear all refresh timers
    for (const [tokenIndex, timer] of this.refreshTimers) {
      clearTimeout(timer);
    }
    this.refreshTimers.clear();
    
    // Clear refreshing state
    this.isRefreshing.clear();
  }
}

// Export singleton instance
export const oauthManager = new OAuthManager();